﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using XNOgre.Graphics;
using Microsoft.Xna.Framework.Graphics;

namespace XNOgre.Core
{
    public enum SortMode
    {
        Direction,
        Distance
    }

    public class BillboardSet : MovableObject, IRenderable
    {

        protected BoundingBox AABB;
        protected float boundingRadius;
        protected Vector2 defaultSize;
        protected bool allDefaultSize;
        protected bool allDefaultRotation;
        protected bool autoExtendPool;
        protected bool sortingEnabled;

        protected List<Billboard> activeBillboards;
        protected Queue<Billboard> freeBillboards;
        protected Billboard[] billboardPool;

        protected DynamicVertexBuffer vertexData;
        protected VertexPositionNormalTexture[] vertices;
        protected IndexBuffer indexData;
        
        protected Camera currentCamera;

        protected Vector4[] textureCoords;

        protected int numVisibleBillboards;

        private bool buffersCreated;
        private int poolSize;
        

        internal BillboardSet(string name, int poolSize = 20)
            :base(name)
        {

            this.boundingRadius = 0.0f;
            this.allDefaultSize = true;
            this.autoExtendPool = true;
            this.sortingEnabled = true;
            this.allDefaultRotation = true;
            this.vertexData = null;
            this.indexData = null;
            this.buffersCreated = false;
            this.poolSize = poolSize;

            activeBillboards = new List<Billboard>();
            freeBillboards = new Queue<Billboard>();

            DefaultDimensions = new Vector2(100, 100);

            this.Material = Material.BaseWhite;
            PoolSize = poolSize;
            Material.CastsShadows = false;
            SetTextureStackAndSlices(1, 1);
            SortMode = Core.SortMode.Distance;
        }
        public override void NotifyCurrentCamera(Camera cam)
        {
            this.currentCamera = cam;
        }
        public override void Dispose()
        {
            for (int i = 0; i < billboardPool.Length; i++)
            {
                billboardPool[i] = null;
            }
            
            DestroyBuffers();
        }
        #region RenderData

        private float SortByDistance(Vector3 sortPos, Billboard bill)
        {
            return -(sortPos - bill.Position).LengthSquared();
        }
        private float SortByDirection(Vector3 sortDir, Billboard bill)
        {
            return Vector3.Dot(sortDir, bill.Direction);
        }
        
        private bool BillboardVisible(Camera cam, Billboard bill)
        {

            Microsoft.Xna.Framework.BoundingSphere sph = new BoundingSphere();
            Matrix xworld = new Matrix();
            xworld = WorldTransforms;
            sph.Center = Vector3.Transform(bill.Position, xworld);

            if (bill.HasOwnDimensions)
            {
                sph.Radius = Math.Max(bill.Dimensions.X, bill.Dimensions.Y);
            }
            else
            {
                sph.Radius = Math.Max(defaultSize.X, defaultSize.Y);
            }

            //TODO: Optimize camera to see if sphere is visible
            return true;
        }
      
        private void IncreasePool(int size)
        {
            Billboard[] oldPool = billboardPool== null ? new Billboard[0] : billboardPool ;

            billboardPool = new Billboard[size];
            for (int i = 0; i < size; i++)
            {
                if (i < oldPool.Length && oldPool[i] != null)
                {
                    billboardPool[i] = oldPool[i];
                }
                else
                {
                    Billboard bill = new Billboard();
                    billboardPool[i] = bill;
                    freeBillboards.Enqueue(bill);
                }
            }
        }
        private void DestroyBuffers()
        {
            if (vertexData != null)
            {
                vertexData.Dispose();
                vertexData = null;
            }

            if (indexData != null)
            {
                indexData.Dispose();
                indexData = null;
            }


            buffersCreated = false;
        }
        protected void CreateBuffers()
        {
            vertices = new VertexPositionNormalTexture[poolSize * 4];
            vertexData = new DynamicVertexBuffer(Root.Instance.RenderSystem.GraphicsDevice, typeof(VertexPositionNormalTexture), poolSize * 4, BufferUsage.WriteOnly);

            short[] indices = new short[poolSize * 6];
            for (int i = 0; i < poolSize; i++)
            {
                indices[i * 6 + 0] = (short)(i * 4 + 0);
                indices[i * 6 + 1] = (short)(i * 4 + 1);
                indices[i * 6 + 2] = (short)(i * 4 + 2);

                indices[i * 6 + 3] = (short)(i * 4 + 2);
                indices[i * 6 + 4] = (short)(i * 4 + 1);
                indices[i * 6 + 5] = (short)(i * 4 + 3);
            }
            
            indexData = new IndexBuffer(Root.Instance.RenderSystem.GraphicsDevice, typeof(short), indices.Length, BufferUsage.WriteOnly);
            indexData.SetData(indices);

            buffersCreated = true;
        }
        public void BeginBillboards(int numBillboards = 0)
        {
            if (!buffersCreated)
                CreateBuffers();
            
            numVisibleBillboards = 0;

        }
        public void EndBillboards()
        {
           //
        }
        public void SetBounds(BoundingBox box, float radius)
        {
            AABB = box;
            boundingRadius = radius;
        }

        #endregion

        public Billboard CreateBillboard(Vector3 position, Color color)
        {
            if (freeBillboards.Count == 0)
            {
                if (autoExtendPool)
                {
                    PoolSize *= 2;
                }
                else
                {
                    return null;
                }
            }

            Billboard newBill = freeBillboards.Dequeue();
            activeBillboards.Add(newBill);
            newBill.Position = position;
            newBill.Color = color;
            newBill.Direction = Vector3.Zero;
            newBill.Rotation = 0;
            newBill.TexCoordIndex = 0;
            newBill.ResetDimensions();
            newBill.NotifyOwner(this);

            float adjust = MathHelper.Max(defaultSize.X, defaultSize.Y);
            Vector3 vecAdjust = new Vector3(adjust);
            Vector3 newMin = position - vecAdjust;
            Vector3 newMax = position + vecAdjust;

            AABB = AABB.Merge(newMin);
            AABB = AABB.Merge(newMin);

            boundingRadius = AABB.GetBoundingRadius();

            return newBill;
        }
        public Billboard GetBillboard(int index)
        {
            return activeBillboards[index];
        }
        public void RemoveBillboard(Billboard bill)
        {
            for (int i = 0; i < activeBillboards.Count; i++)
            {
                if (activeBillboards[i] == bill)
                {
                    RemoveBillboard(activeBillboards[i]);
                    break;
                }
            }
        }
        public void RemoveBillboard(int index)
        {
            freeBillboards.Enqueue(activeBillboards[index]);
            activeBillboards.RemoveAt(index);
        }

        public void Clear()
        { 
            //move actives to free list
            for (int i = 0; i < activeBillboards.Count; i++)
            {
                freeBillboards.Enqueue(activeBillboards[i]);
                activeBillboards.RemoveAt(i);
            }
        }


        internal void NotifyBillboardRotated()
        {
            allDefaultRotation = false;
        }

        internal void NotifyBillboardResized()
        {
            allDefaultSize = false;
        }
        public void UpdateBounds()
        {
            if (activeBillboards.Count == 0)
            {
                AABB = new BoundingBox();
                boundingRadius = 0.0f;
            }
            else
            {
                float maxSqLen = -1.0f;

                Vector3 max, min;
                max = min = Vector3.Zero;

                for (int i = 0; i < activeBillboards.Count; i++)
                {
                    Vector3 pos = activeBillboards[i].Position;

                    min = min.Floor(pos);
                    max = min.Ceil(pos);

                    maxSqLen = Math.Max(maxSqLen, pos.LengthSquared());

                }
                float adjust = Math.Max(defaultSize.X, defaultSize.Y);
                Vector3 vecAdjust = new Vector3(adjust);
                min -= vecAdjust;
                max += vecAdjust;

                AABB = new BoundingBox(min, max);
                boundingRadius = (float)Math.Sqrt(maxSqLen);
            }

            if (parentNode != null)
                parentNode.NeedUpdate();

        }
        public void SortBillboards(Camera cam)
        {
            if (SortMode == Core.SortMode.Direction)
            {
                var sorted = from bill in activeBillboards orderby SortByDirection(-cam.Direction, bill) select bill;
                activeBillboards = new List<Billboard>(sorted);
            }
            else if (SortMode == Core.SortMode.Distance)
            {
                var sorted = from bill in activeBillboards orderby SortByDistance(cam.Position, bill) select bill;
                activeBillboards = new List<Billboard>(sorted);
            }
        }
        public void SetTextureCoords(IEnumerable<Vector4> coords, int numCoords)
        {
            textureCoords = new Vector4[numCoords];
            int counter = 0;
            foreach (var item in coords)
            {
                textureCoords[counter] = item;
                counter++;
            }


        }
        public void SetTextureStackAndSlices(int stacks, int slices)
        {
            if (stacks == 0)
                stacks = 1;

            if (slices == 0)
                slices = 1;

            textureCoords = new Vector4[stacks * slices];
            int coordIndex = 0;

            for (int v = 0; v < stacks; ++v )
            {
                float top = (float)v / (float)stacks;
                float bottom = ((float)v + 1) / (float)stacks;

                for (int u = 0; u < slices; ++u)
                {
                    float left = (float)u / (float)slices;
                    float right = ((float) + 1) / (float)slices;

                    Vector4 r = new Vector4(left, top, right - left, bottom - top);
                    textureCoords[coordIndex] = r;

                    coordIndex++;
                }
            }
        }
        public Vector4 GetTextureCoords(out int numCoords)
        {
            numCoords = textureCoords.Length;
            return textureCoords[0];
        }
        public override RenderOperation RenderOperation
        {
            get
            {
                var rop = new RenderOperation();
                rop.IndexData = indexData;

                vertexData.SetData(vertices);
                rop.VertexData = vertexData;

                rop.OperationType = PrimitiveType.TriangleList;

                rop.VertexCount = numVisibleBillboards * 4;

                rop.PrimCount = (numVisibleBillboards * 2);
                
                return rop;
            }
        }
        public override Matrix WorldTransforms
        {
            get
            {
                return ParentNodeFullTransform;
            }
        }
        public Vector2 DefaultDimensions
        {
            get { return defaultSize; }
            set { defaultSize = value; }
        }
        public float BoundingRadius
        {
            get { return boundingRadius; }
        }
        public override BoundingBox BoundingBox
        {
            get
            {
                return AABB;
            }
        }
        internal override void UpdateRenderQueue(ref List<IRenderable> queue)
        {
            if (sortingEnabled)
            {
                SortBillboards(currentCamera);
            }

            BeginBillboards(activeBillboards.Count);

            foreach (Billboard bb in activeBillboards)
            {
                if (numVisibleBillboards == poolSize)
                    continue;

                if (!BillboardVisible(currentCamera, bb))
                    continue;

                Vector3 up = currentCamera.Up;
                float width = bb.Dimensions.X;
                float height = bb.Dimensions.Y;

                Vector3 pX = Vector3.Transform(Vector3.UnitX, currentCamera.Orientation);
                Vector3 pY = Vector3.Transform(Vector3.UnitY, currentCamera.Orientation);

                Vector3 normal = Vector3.Cross(pX, pY);
                normal.Normalize();

                var left = Vector3.Cross(normal, up);

                var upperCenter = (up * height / 2) + bb.Position;
                var upperLeft = upperCenter + (left * width / 2);
                var upperRight = upperCenter - (left * width / 2);
                var lowerLeft = upperLeft - (up * height);
                var lowerRight = upperRight - (up * height);

                Vector2 textureUpperLeft = new Vector2(0, 0);
                Vector2 textureUpperRight = new Vector2(1f, 0f);
                Vector2 textureLowerLeft = new Vector2(0, 1f);
                Vector2 textureLowerRight = new Vector2(1f, 1f);

                //upper-left
                VertexPositionNormalTexture vertex = new VertexPositionNormalTexture();
                vertex.Position = upperLeft;
                vertex.TextureCoordinate = textureUpperLeft;
                vertex.Normal = normal;
                vertices[numVisibleBillboards * 4 + 0] = vertex;

                //upper-right
                vertex = new VertexPositionNormalTexture();
                vertex.Position = upperRight;
                vertex.TextureCoordinate = textureUpperRight;
                vertex.Normal = normal;
                vertices[numVisibleBillboards * 4 + 1] = vertex;

                //lower-left
                vertex = new VertexPositionNormalTexture();
                vertex.Position = lowerLeft;
                vertex.TextureCoordinate = textureLowerLeft;
                vertex.Normal = normal;
                vertices[numVisibleBillboards * 4 + 2] = vertex;

                //lower-right
                vertex = new VertexPositionNormalTexture();
                vertex.Position = lowerRight;
                vertex.TextureCoordinate = textureLowerRight;
                vertex.Normal = normal;
                vertices[numVisibleBillboards * 4 + 3] = vertex;

                numVisibleBillboards++;
            }
            EndBillboards();

            queue.Add(this);

        }
        public bool AutoExtend
        {
            get { return autoExtendPool; }
            set { autoExtendPool = value; }
        }

        public SortMode SortMode
        {
            get;
            set;
        }
        public bool SortingEnabled
        {
            get { return sortingEnabled; }
            set { sortingEnabled = value; }
        }
        public int PoolSize
        {
            get { return billboardPool.Length; }
            set
            {
                if (billboardPool != null)
                {
                    if (billboardPool.Length >= value)
                        return;
                }
                this.IncreasePool(value);

                for (int i = billboardPool.Length; i < value; i++)
                {
                    freeBillboards.Enqueue(billboardPool[i]);
                }


                poolSize = value;

                DestroyBuffers();
            }
        }
        public virtual int NumBillboards
        {
            get { return activeBillboards.Count; }
        }

        public override float GetSquaredViewDepth(Camera camera)
        {
            return parentNode.GetSquaredViewDepth(camera);
        }
    }
}
